
#include <QDebug>
#include <QFileInfo>
#include <QDir>
#include <QList>
#include <QQueue>
#include <QPair>

#include "taskmanager.h"

#include "utils.h"

class TaskManagerPrivate
{
public:
    TaskManagerPrivate(TaskManager *parent = nullptr);
    ~TaskManagerPrivate();

    bool CheckExec(const QString &exec);
    bool CheckExec(const QFileInfo &exec_fileinfo, bool silence = false);
    void StopCurrentTaskQueue(bool force = false);
    void ClearTasks();
    void ClearTaskQueue();
    void StartTaskQueue();
    void StopTaskQueue(bool waiting = true, bool force = false);

    // SLOTS
    void TaskErrorOccurred(Task *task, QProcess::ProcessError error);
    void TaskFinished(Task *task, int exitcode, QProcess::ExitStatus exitstatus);
    void TaskReadyReadStandardError(Task *task);
    void TaskReadyReadStandardOutput(Task *task);
    void TaskStarted(Task *task);
    void TaskStateChanged(Task *task, QProcess::ProcessState newstate);

    QHash<QString, TaskData> task_nodes;
    QHash<QString, Task*> tasks;
    QQueue<Task*> tasks_queue;
    Task * tasks_queue_current;
    bool is_tasks_queue_running;
private:
    TaskManager * const q_ptr;
    Q_DECLARE_PUBLIC(TaskManager)
};

TaskManagerPrivate::TaskManagerPrivate(TaskManager *parent) : q_ptr(parent)
{
    tasks_queue_current = nullptr;
    is_tasks_queue_running = false;
}

TaskManagerPrivate::~TaskManagerPrivate()
{
    QList<QString> keys = task_nodes.keys();
    for (const QString &key : qAsConst(keys)) {
        TaskData node = task_nodes.take(key);
        node.success_task.clear();
        node.error_task.clear();
    }
    StopCurrentTaskQueue();
    ClearTasks();
    ClearTaskQueue();
}

bool TaskManagerPrivate::CheckExec(const QString &exec)
{
    bool exec_ok = false;
    QFileInfo bin(exec);
    if (bin.isAbsolute() && !CheckExec(bin)) {
        return exec_ok;
    } else {
        QStringList env_pathes = QString(QLatin1String(qgetenv("PATH"))).split(QLatin1String(":"));
        for(const QString &env_path : qAsConst((env_pathes))) {
            QDir dir(env_path);
            QFileInfo bin1 = dir.absoluteFilePath(exec);
            if (CheckExec(bin1, true)) {
                exec_ok = true;
            }
        }
    }
    return exec_ok;
}

bool TaskManagerPrivate::CheckExec(const QFileInfo &exec_fileinfo, bool silence)
{
    if (!silence) {
        qDebug().noquote() << "Check ExecFile:" << exec_fileinfo.filePath();
    }
    if (!exec_fileinfo.exists()) {
        if (!silence) {
            qCritical().noquote() << "Task executable not exist!";
        }
        return false;
    }
    if (!exec_fileinfo.isExecutable()) {
        if (!silence) {
            qCritical().noquote() << "Task executable cannot be executed!";
        }
        return false;
    }
    return true;
}

void TaskManagerPrivate::StopCurrentTaskQueue(bool force)
{
    if (tasks_queue_current) {
        if (tasks_queue_current->is_running()) {
            tasks_queue_current->Stop(force);
        }
        tasks_queue_current->deleteLater();
        tasks_queue_current = nullptr;
    }
}

void TaskManagerPrivate::ClearTasks()
{
    QStringList tasks_uuid = tasks.keys();
    for (const QString &uuid : qAsConst(tasks_uuid)) {
        Task* task = tasks.take(uuid);
        if (task->is_running()) {
            task->Stop(true);
        }
        task->deleteLater();
        task = nullptr;
    }
}

void TaskManagerPrivate::ClearTaskQueue()
{
    while (!tasks_queue.isEmpty()) {
        Task* task = tasks_queue.dequeue();
        task->deleteLater();
        task = nullptr;
    }
}

void TaskManagerPrivate::StartTaskQueue()
{
    if (!tasks_queue.isEmpty()) {
        is_tasks_queue_running = true;
        if (tasks_queue_current) {
            if (tasks_queue_current->is_running()) {
                qCritical().noquote() << "There is a task from queue is running.";
                return;
            }
            tasks_queue_current->deleteLater();
        }
        tasks_queue_current = tasks_queue.dequeue();
        tasks_queue_current->Start();
    }
}

void TaskManagerPrivate::StopTaskQueue(bool waiting, bool force)
{
    Q_Q(TaskManager);
    if (is_tasks_queue_running) {
        is_tasks_queue_running = false;
        if (!waiting) {
            StopCurrentTaskQueue(force);
        }
        ClearTaskQueue();
        emit q->TaskQueueAllDone(false);
    }
}

// Slots
void TaskManagerPrivate::TaskErrorOccurred(Task *task, QProcess::ProcessError error)
{
    Q_UNUSED(error)
    Q_Q(TaskManager);
    const TaskData &data = task_nodes.value(task->data().uuid);

    QString contents = task->errorString();
    qCritical().noquote() << "TaskError:" << data.uuid << "|" << error << "|" << contents << "|";

    emit q->TaskError(data, contents);
}

void TaskManagerPrivate::TaskFinished(Task *task, int exitcode, QProcess::ExitStatus exitstatus)
{
    Q_Q(TaskManager);
    const TaskData &data = task_nodes.value(task->data().uuid);
    QString errorstr;

    switch (exitstatus) {
    case QProcess::NormalExit:
        // qDebug().noquote() << "Task exited normally";
        break;
    case QProcess::CrashExit:
        qCritical().noquote() << "Task crashed";
        errorstr = TaskManager::tr("Task crashed.");
        break;
    }
    if (exitcode != 0) {
        qCritical().noquote() << "Task exit with" << exitcode;
    }

    if (exitcode != 0) {
        if (errorstr.isEmpty()) {
            errorstr = task->errorString();
        } else {
            errorstr = QString(QLatin1String("%1 %2")).arg(errorstr, task->errorString());
        }
    }
    // qDebug().noquote() << __FUNCTION__ << ":" << data.uuid << exitcode << errorstr;

    if (data.uuid.isEmpty()) {
        qWarning().noquote() << __FUNCTION__ << "Task UUID is empty!";
        return;
    }

    qDebug().noquote() << __FUNCTION__ << "UUID:" << data.uuid << "Name:" << data.name << "Count:" << tasks.count() << "Queue:" << tasks_queue.count();

    QStringList uuids = tasks.keys();
    if (uuids.contains(data.uuid)) {
        Task *task = tasks.take(data.uuid);
        if (task->is_running()) {
            task->Stop();
        }
        task->deleteLater();
    }
    emit q->TaskFinished(data, exitcode, errorstr);
    if (tasks_queue.count() == 0) {
        if (is_tasks_queue_running) {
            emit q->TaskQueueAllDone(true);
            is_tasks_queue_running = false;
        }
    }

    if (exitcode == 0) {
        // Create Success Process Task
        if (data.success_task) {
            TaskData success_task = *(data.success_task.data());
            q->CreateTask(success_task);
            if (data.wait_on_success) {
                return;
            }
        }
    } else {
        qCritical().noquote() << "Task exit with error." << "ErrCode:" << exitcode << "," << errorstr;
        // Create Error Process Task
        if (data.error_task) {
            TaskData err_task = *(data.error_task.data());
            q->CreateTask(err_task);
        }
        if (data.stop_on_error) {
            if (tasks_queue.count() > 0) {
                tasks_queue.clear();
            }
        }
    }

    // Task Queue
    if (tasks_queue.count() > 0) {
        q->StartTaskQueue();
    } else {
        if (tasks_queue_current) {
            if (!tasks_queue_current->is_running()) {
                tasks_queue_current->deleteLater();
                tasks_queue_current = nullptr;
            }
        }
    }
}

void TaskManagerPrivate::TaskReadyReadStandardError(Task *task)
{
    Q_Q(TaskManager);
    const TaskData &data = task_nodes.value(task->data().uuid);

    QString contents = QString::fromLocal8Bit(task->readAllStandardError());
#if defined (ENABLE_DEBUG_TASK)
    qDebug().noquote() << "------- Task Raw Error -------";
    qDebug().noquote() << contents;
    qDebug().noquote() << "------------------------------";
#endif
    contents = contents.remove(QLatin1String("\r"));
    qCritical().noquote() << "TaskError:" << data.uuid << "|" << contents << "|";

    emit q->TaskError(data, contents);
}

void TaskManagerPrivate::TaskReadyReadStandardOutput(Task *task)
{
    Q_Q(TaskManager);
    const TaskData &data = task_nodes.value(task->data().uuid);

    QString contents = QString::fromLocal8Bit(task->readAllStandardOutput());
#if defined (ENABLE_DEBUG_TASK)
    qDebug().noquote() << "======= Task Raw Output =======";
    qDebug().noquote() << contents;
    qDebug().noquote() << "===============================";
#endif
    contents = contents.remove(QLatin1String("\r"));
    // qDebug().noquote() << "TaskOutput:" << data.uuid << "|" << contents << "|";

    emit q->TaskOutput(data, contents);
}

void TaskManagerPrivate::TaskStarted(Task *task)
{
    Q_Q(TaskManager);
    const TaskData &data = task_nodes.value(task->data().uuid);

    emit q->TaskStarted(data);
}

void TaskManagerPrivate::TaskStateChanged(Task *task, QProcess::ProcessState newstate)
{
    const TaskData &data = task_nodes.value(task->data().uuid);

    switch (newstate) {
    case QProcess::ProcessState::NotRunning:
        qDebug().noquote() << "Task not running";
        break;
    case QProcess::ProcessState::Starting:
        qDebug().noquote() << "Task starting" << "[" << data.exec << data.args << "]";
        break;
    case QProcess::ProcessState::Running:
        qDebug().noquote() << "Task running";
        break;
    default: break;
    }
}

TaskManager::TaskManager(QObject *parent) : QObject{parent}, d_ptr(new TaskManagerPrivate(this))
{
    RegisterMetaTypes(TaskData);
}

TaskManager::~TaskManager()
{
}

bool TaskManager::CreateTask(const TaskData &taskdata, bool is_queue)
{
    Q_D(TaskManager);

    TaskData data = taskdata;
    if (data.uuid.isEmpty()) {
        data.uuid = QUuid::createUuid().toString();
    }

    // qDebug().noquote() << __FUNCTION__ << ":" << data.uuid << "-" << data.name << ":" << data.exec << "," << data.args << "," << is_queue;
    if (data.exec.isEmpty()) {
        return false;
    }

    if (!d->CheckExec(data.exec)) {
        return false;
    }
    d->task_nodes.insert(data.uuid, data);

    Task *task = new Task(data, this);
    connect(task, &QProcess::errorOccurred, this, [=](QProcess::ProcessError error) { d->TaskErrorOccurred(task, error); });
    connect(task, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, [=](int code, QProcess::ExitStatus status) { d->TaskFinished(task, code, status); });
    connect(task, &QProcess::readyReadStandardError, this, [=]() { d->TaskReadyReadStandardError(task); });
    connect(task, &QProcess::readyReadStandardOutput, this, [=]() { d->TaskReadyReadStandardOutput(task); });
    connect(task, &QProcess::started, this, [=]() { d->TaskStarted(task); });
    connect(task, &QProcess::stateChanged, this, [=](QProcess::ProcessState newstate) { d->TaskStateChanged(task, newstate); });

    if (is_queue) {
        d->tasks_queue.enqueue(task);
    } else {
        d->tasks.insert(task->uuid(), task);
        task->Start();
    }

    return true;
}

bool TaskManager::CreateTaskQueue(const TaskQueue &tasks)
{
    for (const TaskData &task : tasks) {
        if (!CreateTask(task, true)) {
            return false;
        }
    }
    return true;
}

bool TaskManager::AttachTaskQueue(const TaskData &taskdata)
{
    return CreateTask(taskdata, true);
}

void TaskManager::StartTaskQueue()
{ Q_D(TaskManager); d->StartTaskQueue(); }

void TaskManager::StopTaskQueue(bool waiting, bool force)
{ Q_D(TaskManager); d->StopTaskQueue(waiting, force); }
